Skip to content

feat: guardedFetch + sameSitePolicy — runtime-agnostic guarded fetch#10

Merged
jlc488 merged 1 commit into
mainfrom
feat/guarded-fetch
Jul 12, 2026
Merged

feat: guardedFetch + sameSitePolicy — runtime-agnostic guarded fetch#10
jlc488 merged 1 commit into
mainfrom
feat/guarded-fetch

Conversation

@jlc488

@jlc488 jlc488 commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

What

The Workers pattern the README told consumers to hand-roll (#9) now ships as API — AskLinq's URL ingestion was the first consumer to write that loop by hand, which was the signal it belongs in the package.

guardedFetch(input, policy, init?)

URL-time validation + the same redirect semantics as safeFetch — every hop re-passes the policy before it is followed, credentials stripped on cross-origin redirects, 303/301/302-POST downgrade to GET without body replay — minus DNS resolution and pinning. Works on Cloudflare Workers, browsers, and edge runtimes. fetchImpl is injectable for tests/instrumentation.

The policy allowlist is the primary control: the fail-closed default (empty allowlist allows nothing) does real work here. Opening a specific host = putting it in the allowlist — the bypass mechanism stays in the policy, auditable in one place.

sameSitePolicy(url, overrides?)

Derives the allowlist from a submitted URL: the whole fetch, redirects included, locks to that domain (www. stripped so apex ↔ www redirects survive; subdomains reachable). Overrides merge additively, so extra hosts can be allowlisted alongside.

Refactor

  • The redirect-revalidation loop moved verbatim from safeFetch into redirect.ts (zero Node imports); both fetchers share it. safeFetch layers its DNS hooks on top (unpinned per-hop check / pinned undici connector / guard-error unwrap).
  • node:dns is now lazily imported — loading the package no longer requires a functional dns module; calling safeFetch without one throws a typed SsrfGuardError pointing to guardedFetch.

Docs

README (en + ko): Workers section now shows guardedFetch/sameSitePolicy instead of the DIY loop; support matrix gains the new row. CHANGELOG gets an Unreleased section.

Verification

  • tsc --noEmit clean, build clean
  • 136/136 tests — 12 new (fail-closed empty allowlist, redirect escape blocked without fetching the target, credential stripping, 303 downgrade, maxRedirects, sameSitePolicy derivation/merging/typed errors); the untouched safe-fetch suites (18 unpinned + 5 pinned) pass unchanged against the shared loop

After release

AskLinq's ingest/url.ts swaps its hand-rolled guardedFetchPage loop for guardedFetch + sameSitePolicy.

Workers/browser/edge consumers had to hand-roll the manual-redirect
loop the README described (AskLinq did exactly that). The pattern now
ships as API:

- guardedFetch(input, policy, init?) — URL-time validation plus the
  same redirect semantics as safeFetch (every hop re-passes the policy
  before it is followed, credentials stripped on cross-origin
  redirects, 303/301/302-POST downgrade to GET without body replay),
  minus DNS resolution and pinning. The policy allowlist is the
  primary control; the package's fail-closed default (empty allowlist
  allows nothing) does real work here. fetchImpl is injectable.
- sameSitePolicy(url, overrides?) — derive the allowlist from a
  submitted URL: the whole fetch, redirects included, locks to that
  domain (www stripped so apex<->www redirects survive). Overrides
  merge additively so specific extra hosts can be allowlisted — the
  'bypass' mechanism stays in the policy, auditable in one place.

Refactor: the redirect-revalidation loop moved verbatim from safeFetch
into redirect.ts (no Node imports) and both fetchers share it —
safeFetch layers its DNS hooks (unpinned per-hop check, pinned undici
connector, guard-error unwrap) on top. node:dns is now imported
lazily, so loading the package no longer requires a functional dns
module; calling safeFetch without one throws a typed SsrfGuardError
pointing to guardedFetch.

README (en+ko): Workers section now shows guardedFetch/sameSitePolicy
instead of the DIY loop; support matrix gains the new row.

Tests: 136/136 green — 12 new (allowlist fail-closed, redirect escape
blocked without fetching, credential stripping, 303 downgrade,
maxRedirects, sameSitePolicy derivation/merging/typed errors); the
untouched safe-fetch suites (18 unpinned + 5 pinned) pass unchanged
against the shared loop.
@jlc488
jlc488 merged commit b4b5f48 into main Jul 12, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant